/**
 * Copyright (C) 2010-2011 J.W.Marsden
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/
package cc.plural.jsonij.marshal;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import cc.plural.jsonij.Value;

/**
 *
 * @author openecho
 */
public class JSONCodecStore {

    public static final String CREATE_INSTANCE_METHOD_SIGNATURE;
    public static final String ENCODE_METHOD_SIGNATURE;
    public static final String DECODE_METHOD_SIGNATURE;
    List<Class<?>> codecList;
    Map<Class<?>, JSONCodec<?>> codecInstanceMap;

    static {
        CREATE_INSTANCE_METHOD_SIGNATURE = "createInstance";
        ENCODE_METHOD_SIGNATURE = "encode";
        DECODE_METHOD_SIGNATURE = "decode";
    }

    public JSONCodecStore() {
        codecList = new ArrayList<Class<?>>();
        codecInstanceMap = new HashMap<Class<?>, JSONCodec<?>>();
    }

    public int size() {
        return codecList.size();
    }

    public boolean isEmpty() {
        return codecList.isEmpty();
    }

    public void printSuperClasses(Class<?> c) {
        String pad = "";
        do {
            String typeString = "";
            TypeVariable<?>[] types = c.getTypeParameters();
            for (TypeVariable<?> type : types) {
                typeString += type.getBounds()[0];
            }
            System.out.println(String.format("%s%s<%s>", pad, c, typeString));
            pad += " ";
        } while ((c = c.getSuperclass()) != null);
    }

    public Class<?>[] getTypeParameterSet(Class<?> codec) {
        /**
         * TODO: yer....
         */
        Class<?>[] typeParameterSet = null;
        Class<?> typeParameter = null;
        TypeVariable<?>[] types = codec.getTypeParameters();
        for (TypeVariable<?> type : types) {
            typeParameter = (Class<?>) type.getBounds()[0];
        }
        typeParameterSet = new Class<?>[]{typeParameter};
        return typeParameterSet;
    }

    public void registerCodec(Class<?> codec) {
        Object codecInstance = null;
        Method genericCodecCreateMethod = null;
        Class<?> codecType = null;
        JSONCodec<?> genericCodecInstance = null;
        try {
            codecInstance = codec.newInstance();
            genericCodecCreateMethod = codec.getMethod(CREATE_INSTANCE_METHOD_SIGNATURE);
            genericCodecInstance = (JSONCodec<?>) genericCodecCreateMethod.invoke(codecInstance);
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(JSONCodecStore.class.getName()).log(Level.SEVERE, null, ex);
        } catch (InvocationTargetException ex) {
            Logger.getLogger(JSONCodecStore.class.getName()).log(Level.SEVERE, null, ex);
        } catch (NoSuchMethodException ex) {
            Logger.getLogger(JSONCodecStore.class.getName()).log(Level.SEVERE, null, ex);
        } catch (SecurityException ex) {
            Logger.getLogger(JSONCodecStore.class.getName()).log(Level.SEVERE, null, ex);
        } catch (InstantiationException ex) {
            Logger.getLogger(JSONCodecStore.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            Logger.getLogger(JSONCodecStore.class.getName()).log(Level.SEVERE, null, ex);
        }

        genericCodecInstance.setCodecHash(codec.hashCode());

        /*
        if (genericCodecInstance != null) {
        } else {
            // TODO: Throw Exception
        }
        */
        
        Method encodeMethod = null;
        Method decodeMethod = null;
        Class<?>[] typeParameterSet = getTypeParameterSet(codec);
        if (typeParameterSet == null || Array.getLength(typeParameterSet) == 0) {
            typeParameterSet = new Class<?>[]{java.lang.Object.class};
        }

        for (Class<?> typeParameter : typeParameterSet) {
            try {
                encodeMethod = genericCodecInstance.getClass().getMethod(ENCODE_METHOD_SIGNATURE, typeParameter);
            } catch (NoSuchMethodException ex) {
                Logger.getLogger(JSONCodecStore.class.getName()).log(Level.SEVERE, null, ex);
            } catch (SecurityException ex) {
                Logger.getLogger(JSONCodecStore.class.getName()).log(Level.SEVERE, null, ex);
            }
            if (encodeMethod != null) {
                codecType = typeParameter;
                break;
            }
        }

        try {
            decodeMethod = genericCodecInstance.getClass().getMethod(DECODE_METHOD_SIGNATURE, Value.class);
        } catch (NoSuchMethodException ex) {
            Logger.getLogger(JSONCodecStore.class.getName()).log(Level.SEVERE, null, ex);
        } catch (SecurityException ex) {
            Logger.getLogger(JSONCodecStore.class.getName()).log(Level.SEVERE, null, ex);
        }

        if (encodeMethod != null && decodeMethod != null) {
            genericCodecInstance.setEncodeMethod(encodeMethod);
            genericCodecInstance.setDecodeMethod(decodeMethod);
            codecList.add(codec);
            codecInstanceMap.put(codecType, genericCodecInstance);
        }
    }

    public void registerCodec(Class<?> type, Class<?> codec) {
    }

    public void deregisterCodec(Class<?> codec) {
        if (codecList.contains(codec)) {
            int codecHash = codec.hashCode();
            Iterator<Class<?>> codecInstanceIterator = codecInstanceMap.keySet().iterator();
            while (codecInstanceIterator.hasNext()) {
                Class<?> codecType = codecInstanceIterator.next();
                JSONCodec<?> codecInstance = codecInstanceMap.get(codecType);
                if (codecInstance.getCodecHash() == codecHash) {
                    codecInstanceMap.remove(codecType);
                    codecList.remove(codec);
                    break;
                }
            }
        }

    }

    /**
     * Check if the codec store has a codec registered for the class specified.
     * If a codec is found then this will return true. The check will test the
     * specified class then all parents of the specified class.
     * @param type Class instance to check for.
     * @return true when codec is found or false when there is not a codec.
     */
    public boolean hasCodec(Class<?> type) {
        if (codecInstanceMap.containsKey(type)) {
            return true;
        } else {
            // Check parent types
            Class<?> parent = type.getSuperclass();
            do {
                if (codecInstanceMap.containsKey(parent)) {
                    return true;
                }
            } while ((parent = parent.getSuperclass()) != null);
            return false;
        }
    }

    /**
     * Finds the codec for the specified type. The check will check if a codec exists for the
     * specified class then all parents of the specified class. When it finds a codec
     * it will return it.
     * @param type The Class type to check for.
     * @return JSONCodec the codec for the specified class or null if none is found.
     */
    public JSONCodec<?> getCodec(Class<?> type) {
        if (codecInstanceMap.containsKey(type)) {
            return codecInstanceMap.get(type);
        } else {
            Class<?> parent = type.getSuperclass();
            do {
                if (codecInstanceMap.containsKey(parent)) {
                    return codecInstanceMap.get(parent);
                }
            } while ((parent = type.getSuperclass()) != null);
            return null;
        }
    }
}
